<?php
/**
 * Statement
 *
 * @category	DataSource
 * @package		dayscript.datasource.sql
 * @subpackage
 * @author		Nelson Daza <ndaza@dayscript.com>
 * @copyright	2009 Dayscript Ltda.
 * @license
 * @version		1.0
 * @version		$Revision: 0 $
 * @filesource
 * @link		http://www.dayscript.com
 * @link		{docsLink}
 * @uses
 * @since		1.0
 * @modifiedby	$LastChangedBy: Nelson Daza $
 * @modified	$Date: 2009-09-01 $
 */

namespace dayscript\datasource\sql;

/**
 * Statement
 *
 * @category	DataSource
 * @package		dayscript.datasource.sql
 * @subpackage
 * @author		Nelson Daza <ndaza@dayscript.com>
 * @copyright	2009 Dayscript Ltda.
 * @license
 * @version		1.0
 * @link		http://www.dayscript.com
 * @link		{docsLink}
 * @uses		\dayscript\datasource\sql\Database
 * @since		1.0
 * @modifiedby	$LastChangedBy: Nelson Daza $
 * @modified	$Date: 2009-09-02 $
 */
abstract class Statement	{
	/**
	 * Statement adapter for connection use
	 * @var \dayscript\datasource\sql\Database $adapter
	 */
	protected $adapter = null;
	/**
	 * The current fetch mode.
	 * @var integer $fetchMode
	 */
	protected $fetchMode = \dayscript\datasource\sql\Database::FETCH_ASSOC;
	/**
	 * Statement attributes
	 * @var array $attributes
	 */
	protected $attributes = array( );
	/**
	 * Column result bindings.
	 * @var array $bindColumn
	 */
	protected $bindColumn = array( );
	/**
	 * Query parameter bindings; covers bindParam() and bindValue().
	 * @var array $bindParam
	 */
	protected $bindParam = array( );
	/**
	 * SQL string split into an array at placeholders.
	 * @var array $sqlSplit
	 */
	protected $sqlSplit = array( );
	/**
	 * Parameter placeholders in the SQL string by position in the split array.
	 * @var array $sqlParam
	 */
	protected $sqlParam = array( );

	/**
	 * Statement constructor
	 * @param \dayscript\datasource\sql\Database $adapter
	 * @param mixed $sql Either a string or Select.
	 * @return Statement
	 * TODO SELECT
	 */
	public function __construct( \dayscript\datasource\sql\Database $adapter, $sql )	{
		$this->adapter = $adapter;
		if ($sql instanceof \dayscript\datasource\sql\Select )
			$sql = $sql->assemble( );
		
		$this->adapter->log( 'SQL: ' . $sql );
		
		$this->parseParameters( $sql );
		$this->prepare( $sql );
		
	}
	/**
	 * Statement Prepare
	 * @param string $sql
	 */
	public abstract function prepare( $sql );
	/**
	 * @param string $sql
	 * @return void
	 */
	protected final function parseParameters( $sql )	{
		$sql = $this->stripQuoted( $sql );

		$this->sqlSplit = \preg_split( '/(\?|\:[a-zA-Z0-9_]+)/', $sql, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY );
		$this->sqlParam = array( );
		
		foreach ( $this->sqlSplit as $key => $val )	{
			if ( $val == '?' )	{
				if ( !$this->adapter->supportsParameters( 'positional' ) )
					throw new \Exception( 'Invalid bind-variable position \'' . $val . '\'.' );
			}
			else if ( $val[0] == ':' )	{
				if ( !$this->adapter->supportsParameters( 'named' ) )
					throw new \Exception( 'Invalid bind-variable name \'' . $val . '\'.' );
			}
			$this->sqlParam[] = $val;
		}
		$this->bindParam = array( );
	}
	/**
	 * Remove parts of a SQL string that contain quoted strings of values or identifiers.
	 *
	 * @param string $sql
	 * @return string
	 */
	protected final function stripQuoted( $sql )	{
		$quoteChar = $this->adapter->getQuoteValueSymbol( );
		$quoteEscapeChar = \str_replace( '\\', '\\\\', \str_replace( $quoteChar, '', $this->adapter->quote( $quoteChar ) ) );

		// Remove all quoted values and delimited identifiers
		$sql = \preg_replace( "/$quoteChar($quoteEscapeChar|\\\\{2}|[^$quoteChar])*$quoteChar/", '', $sql );
		if ( !$quoteChar )
			$sql = \preg_replace( "/$quoteChar($quoteEscapeChar|[^$quoteChar])*$quoteChar/", '', $sql );

		return $sql;
	}
	/**
	 * Bind a column of the statement result set to a PHP variable.
	 *
	 * @param string $column Name the column in the result set, either by position or by name.
	 * @param mixed $param Reference to the PHP variable containing the value.
	 * @param mixed $type OPTIONAL
	 * @return bool
	 */
	public final function bindColumn( $column, &$param, $type = null )	{
		$this->bindColumn[$column] =& $param;
		return true;
	}
	/**
	 * Binds a parameter to the specified variable name.
	 *
	 * @param mixed $parameter Name the parameter, either integer or string.
	 * @param mixed $variable Reference to PHP variable containing the value.
	 * @param mixed $type OPTIONAL Datatype of SQL parameter.
	 * @param mixed $length OPTIONAL Length of SQL parameter.
	 * @param mixed $options OPTIONAL Other options.
	 * @return bool
	 */
	public final function bindParam( $parameter, &$variable, $type = null, $length = null, $options = null )	{
		if ( !\is_int( $parameter ) && !\is_string( $parameter ) )
			throw new \InvalidArgumentException( 'Invalid bind-variable position.' );

		$position = null;
		$intval = (int)$parameter;
		if ( $intval > 0 && $this->adapter->supportsParameters( 'positional' ) )	{
			if ( $intval >= 1 || $intval <= \count( $this->sqlParam ) )
				$position = $intval;
		}
		else if ( $this->adapter->supportsParameters( 'named' ) )	{
			if ( $parameter[0] != ':' )
				$parameter = ':' . $parameter;
			if ( \in_array( $parameter, $this->sqlParam ) !== false )
				$position = $parameter;
		}
		if ( $position === null )
			throw new \Exception( 'Invalid bind-variable position "' . $parameter . '".' );

		$this->bindParam[$position] =& $variable;
		return $this->statementBindParam( $position, $variable, $type, $length, $options );
	}
	/**
	 * Binds a parameter to the specified variable name.
	 *
	 * @param mixed $parameter Name the parameter, either integer or string.
	 * @param mixed $variable Reference to PHP variable containing the value.
	 * @param mixed $type OPTIONAL Datatype of SQL parameter.
	 * @param mixed $length OPTIONAL Length of SQL parameter.
	 * @param mixed $options OPTIONAL Other options.
	 * @return bool
	 */
	protected abstract function statementBindParam( $parameter, &$variable, $type = null, $length = null, $options = null );
	/**
	 * Binds a value to a parameter.
	 *
	 * @param mixed $parameter Name the parameter, either integer or string.
	 * @param mixed $value Scalar value to bind to the parameter.
	 * @param mixed $type OPTIONAL Datatype of the parameter.
	 * @return bool
	 */
	public final function bindValue( $parameter, $value, $type = null )	{
		return $this->bindParam( $parameter, $value, $type );
	}
	/**
	 * Executes a prepared statement.
	 *
	 * @param array $params OPTIONAL Values to bind to parameter placeholders.
	 * @return bool
	 */
	public final function execute( array $params = null )	{
		return $this->statementExecute( $params );
	}
	/**
	 * Executes a prepared statement.
	 *
	 * @param array $params OPTIONAL Values to bind to parameter placeholders.
	 * @return bool
	 */
	protected abstract function statementExecute( array $params = null );
	/**
	 * Returns an array containing all of the result set rows.
	 *
	 * @param int $style OPTIONAL Fetch mode.
	 * @param int $col OPTIONAL Column number, if fetch mode is by column.
	 * @return array Collection of rows, each in a format by the fetch mode.
	 */
	public final function fetchAll( $style = null, $col = null )	{
		$data = array( );
		if ( $style === \dayscript\datasource\sql\Database::FETCH_COLUMN && $col === null )
			$col = 0;
		
		if ( $col === null )	{
			while ( ( $row = $this->fetch( $style ) ) != null )
				$data[] = $row;
		}
		else	{
			while ( ( $val = $this->fetchColumn( $col ) ) != false )
				$data[] = $val;
		}
		return $data;
	}
	/**
	 * Returns a single column from the next row of a result set.
	 *
	 * @param int $col OPTIONAL Position of the column to fetch.
	 * @return string One value from the next row of result set, or false.
	 */
	public function fetchColumn( $col = 0 )	{
		$data = array( );
		$col = (int)$col;
		$row = $this->fetch( \dayscript\datasource\sql\Database::FETCH_NUM );
		return ( \is_array( $row ) ? $row[$col] : null );
	}
	/**
	 * Fetches the next row and returns it as an object.
	 *
	 * @param string $class  OPTIONAL Name of the class to create.
	 * @param array  $config OPTIONAL Constructor arguments for the class.
	 * @return mixed One object instance of the specified class, or false.
	 */
	public function fetchObject( $class = 'stdClass', array $config = array( ) )	{
		$obj = new $class( $config );
		$row = $this->fetch( \dayscript\datasource\sql\Database::FETCH_ASSOC );
		if ( !\is_array( $row ) )
			return false;
		foreach ( $row as $key => $val )
			$obj->$key = $val;
		return $obj;
	}
	/**
	 * Retrieve a statement attribute.
	 *
	 * @param string $key Attribute name.
	 * @return mixed Attribute value.
	 */
	public final function getAttribute( $key )	{
		return ( \array_key_exists( $key, $this->attributes ) ? $this->attributes[$key] : null );
	}
	/**
	 * Set a statement attribute.
	 *
	 * @param string $key Attribute name.
	 * @param mixed $val Attribute value.
	 * @return bool
	 */
	public final function setAttribute( $key, $val )	{
		$this->attributes[$key] = $val;
	}
	/**
	 * Set the default fetch mode for this statement.
	 *
	 * @param int $mode The fetch mode.
	 * @return bool
	 */
	public function setFetchMode( $mode )	{
		switch ( $mode )	{
			case \dayscript\datasource\sql\Database::FETCH_NUM:
			case \dayscript\datasource\sql\Database::FETCH_ASSOC:
			case \dayscript\datasource\sql\Database::FETCH_BOTH:
			case \dayscript\datasource\sql\Database::FETCH_OBJ:
				$this->fetchMode = $mode;
				break;
			case \dayscript\datasource\sql\Database::FETCH_BOUND:
			default:
				$this->closeCursor( );
				throw new \InvalidArgumentException( 'Invalid fetch mode.');
				break;
		}
	}
	/**
	 * Closes the cursor, allowing the statement to be executed again.
	 * @return bool
	 */
	public abstract function closeCursor( );
	/**
	 * Closes the cursor and the statement.
	 * @return bool
	 */
	public abstract function close( );
	/**
	 * Helper function to map retrieved row to bound column variables
	 * @param array $row
	 * @return bool True
	 */
	public final function fetchBound( $row )	{
		foreach ( $row as $key => $value )	{
			// bindColumn() takes 1-based integer positions
			// but fetch() returns 0-based integer indexes
			if ( \is_int( $key ) )
				$key++;
			// set results only to variables that were bound previously
			if ( isset( $this->bindColumn[$key] ) )
				$this->bindColumn[$key] = $value;
		}
		return true;
	}
	/**
	 * Gets the Database Adapter for this particular Statement object.
	 * @return \dayscript\datasource\sql\Database
	 */
	public final function getAdapter( )	{
		return $this->adapter;
	}
	/**
	 * Returns the number of columns in the result set.
	 * @return int The number of columns.
	 */
	public abstract function columnCount( );
	/**
	 * Returns the number of rows affected by the execution of the last INSERT, DELETE, or UPDATE statement executed.
	 * @return int The number of rows affected.
	 */
	public abstract function rowCount( );
	/**
	 * Retrieves the error code, if any, associated with the last operation on the statement handle.
	 * @return string error code.
	 */
	public abstract function errorCode( );
	/**
	 * Retrieves an array of error information, if any, associated with the last operation on the statement handle.
	 * @return array|null
	 */
	public abstract function errorInfo( );
	/**
	 * Fetches a row from the result set.
	 * @param int $style OPTIONAL Fetch mode for this fetch operation.
	 * @param int $cursor OPTIONAL Absolute, relative, or other.
	 * @param int $offset OPTIONAL Number for absolute or relative cursors.
	 * @return mixed Array, object, or scalar depending on fetch mode.
	 */
	public abstract function fetch( $style = null, $cursor = null, $offset = null );
	/**
	 * Retrieves the next rowset (result set) for a SQL statement that has multiple result sets.
	 * An example is a stored procedure that returns the results of multiple queries.
	 * @return bool
	 */
	public abstract function nextRowset( );
}
